# Generic `EachValidator` that validates account names on various communication
# services such as Skype, Yahoo!, etc. This class provides a simple DSL for
# describing valid account names on these sites, performing all validation for
# you.
#
# Subclass this class and use the provided DSL to describe account names on a
# given site. An example for a fictional service called Talkalot:
#
# ```` ruby
# class TalkalotValidator < AccountNameValidator
#   min_length 5
#   max_length 64
#   valid_chars "A-Z0-9_"
# end
# ````
#
# With your validator defined you can now use it in your Active Record models
# like any other `EachValidator`:
#
# ```` ruby
#   validates :talkalot_id,
#             talkalot: true
# ````
#
# This class automatically handles the following options passed to the
# `validates` method:
#
# |              |                                 |
# |:-------------|:--------------------------------|
# | `:allow_nil` | Allows `nil` values.            |
# | `:message`   | Provide a custom error message. |
#
# Error messages generated by this class are stored in the translation table.
# The localization keys used are generated by the
# `ActiveModel::Errors#generate_message` method (see its documentation for more
# information). The lastmost element of the localization key is the error
# message key. The error message key is a combination of a validator subclass's
# {.error_key_prefix} and the error key suffix for a given constraint.
#
# By default the error key prefix is the underscored
#
# As an example, for the `TalkalotValidator` example above, the error message
# key used in the event that a two-letter account name is given would be
# `:talkalot_too_short`. If you wanted to override the prefix, you could do:
#
# ```` ruby
# class TalkalotValidator < AccountNameValidator
#   error_key_prefix :talky
# end
# ````
#
# In this case the error message key for a two-letter account name would be
# `:talky_too_short`. (You'd do this if Talkalot accounts were called "talkies,"
# for example.)
#
# @abstract Subclass this validator to perform your specific account name
#   validations.

class AccountNameValidator < ActiveModel::EachValidator
  class_attribute :validations
  self.validations = Array.new

  # @private
  def validate_each(record, attribute, value)
    return if options[:allow_nil] and value.nil?
    return if options[:allow_blank] and value.blank?
    return unless self.class.validations
    self.class.validations.each { |block, key| record.errors.add(attribute, options[:message] || record.errors.generate_message(attribute, :"#{self.class.error_key_prefix}_#{key}")) if !block[value.to_s] }
  end

  protected

  # @overload error_key_prefix
  #   Returns the prefix for error message keys used by this class.
  #   @return [Symbol] The error message key prefix.
  #
  # @overload error_key_prefix(value)
  #   Sets the error message key prefix this class uses.
  #   @param [Symbol] value The new error message key prefix.

  def self.error_key_prefix(value=nil)
    if value then
      @error_key_prefix = value
    else
      return @error_key_prefix || to_s.demodulize.sub(/Validator$/, '').underscore.to_sym
    end
  end

  # Describes a custom restraint on account names.
  #
  # @param [Symbol] key The error message key suffix to use.
  # @yield [value] The custom validation.
  # @yieldparam [String] value The value to validate. This will have been
  #   coerced into a `String`.
  # @yieldreturn [true, false] Whether or not the validation succeeded.

  def self.add_validation(key, &block)
    return unless block_given?
    self.validations += [[ block, key ]]
  end

  # Enforces a minimum length on account names. Uses the "too_short" error
  # message key suffix.
  #
  # @param [Fixnum] num The minimum number of characters.

  def self.min_length(num)
    add_validation(:too_short) { |value| value.length >= num }
  end

  # Enforces a maximum length on account names. Uses the "too_long" error
  # message key suffix.
  #
  # @param [Fixnum] num The maximum number of characters.

  def self.max_length(num)
    add_validation(:too_long) { |value| value.length <= num }
  end

  # Enforces a valid set of characters. Uses the "invalid_chars" error message
  # key suffix.
  #
  # @param [String] charlist A set of valid characters, in regex character class
  #   format (e.g., "[A-Z0-9_]").

  def self.valid_chars(charlist)
    add_validation(:invalid_chars) { |value| value =~ /^[#{charlist}]+$/ }
  end

  # Enforces a valid set of characters for the first character of the account
  # name. Uses the "invalid_first_char" error message key suffix.
  #
  # @param [String] charlist A set of valid characters, in regex character class
  #   format (e.g., "[A-Z0-9_]").

  def self.first_char(charlist)
    add_validation(:invalid_first_char) { |value| value[0] =~ /^[#{charlist}]$/ }
  end
end
